﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Markup;
using TextPlus.Extensions;

namespace TextPlus
{
    [TemplatePart(Type = typeof(StackPanel), Name = NAME_LINES_PANEL)]
    [ContentProperty("Inlines")]
    public class TextBlockPlus : ItemsControl
    {
        private const string NAME_LINES_PANEL = "PART_LinesPanel";

        protected StackPanel Lines;

        public static DependencyProperty TextProperty = DependencyProperty.Register(
            "Text", 
            typeof (string),
            typeof (TextBlockPlus),
            new PropertyMetadata(Text_Changed));

        public static DependencyProperty TextIndentProperty = DependencyProperty.Register(
            "TextIndent", 
            typeof(double),
            typeof(TextBlockPlus),
            new PropertyMetadata(0d, TextIndent_Changed));

        public static DependencyProperty AutoHyperlinkingEnabledProperty = DependencyProperty.Register(
            "AutoHyperlinkingEnabled", 
            typeof(bool), 
            typeof(TextBlockPlus),
            new PropertyMetadata(true, AutoHyperlinkingEnabled_Changed));

        public static DependencyProperty TextWrappingProperty = DependencyProperty.Register(
            "TextWrapping",
            typeof (TextWrapping),
            typeof (TextBlockPlus),
            new PropertyMetadata(TextWrapping.NoWrap, TextWrappingPropertyChanged));

        public static DependencyProperty LinkStyleProperty = DependencyProperty.Register(
            "LinkStyle",
            typeof (Style),
            typeof (TextBlockPlus),
            new PropertyMetadata(null, LinkStylePropertyChanged));

        private readonly ObservableCollection<object> _inlines = new ObservableCollection<object>();

        public event RoutedEventHandler LinkClicked;

        public TextBlockPlus()
        {
            DefaultStyleKey = typeof(TextBlockPlus);
            _inlines.CollectionChanged += _inlines_CollectionChanged;
        }

        void _inlines_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            if (HasInlines() && !string.IsNullOrEmpty(Text))
            {
                Text = null;
                throw new Exception("Can't have both Inlines and Text values at the same time");
            }
            InvalidateMeasure();
        }

        private static void Text_Changed(DependencyObject o, DependencyPropertyChangedEventArgs args)
        {
            var hyperTextBlock = (TextBlockPlus)o;
            if (!string.IsNullOrEmpty(hyperTextBlock.Text) && hyperTextBlock.HasInlines())
            {
                hyperTextBlock.Inlines.Clear();
                throw new Exception("Can't have both Inlines and Text values at the same time");
            }
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            Lines = (StackPanel)GetTemplateChild(NAME_LINES_PANEL);
        }

        private static void TextIndent_Changed(DependencyObject o, DependencyPropertyChangedEventArgs args)
        {
            ((TextBlockPlus)o).InvalidateMeasure();
        }

        private static void AutoHyperlinkingEnabled_Changed(DependencyObject o, DependencyPropertyChangedEventArgs args)
        {
            ((TextBlockPlus)o).InvalidateMeasure();
        }

        private static void TextWrappingPropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs args)
        {
            ((TextBlockPlus) o).InvalidateMeasure();
        }

        private static void LinkStylePropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs args)
        {
            ((TextBlockPlus)o).InvalidateMeasure();
        }

        public string Text
        {
            get { return (string) GetValue(TextProperty); }
            set { SetValue(TextProperty, value); }
        }

        public double TextIndent
        {
            get { return (double)GetValue(TextIndentProperty); }
            set { SetValue(TextIndentProperty, value); }
        }

        public bool AutoHyperlinkingEnabled
        {
            get { return (bool)GetValue(AutoHyperlinkingEnabledProperty); }
            set { SetValue(AutoHyperlinkingEnabledProperty, value); }
        }

        public TextWrapping TextWrapping
        {
            get { return (TextWrapping) GetValue(TextWrappingProperty); }
            set { SetValue(TextWrappingProperty, value); }
        }

        public Style LinkStyle
        {
            get { return (Style) GetValue(LinkStyleProperty); }
            set { SetValue(LinkStyleProperty, value); }
        }

        public ObservableCollection<object> Inlines
        {
            get { return _inlines; }
        }

        private bool HasInlines()
        {
            if (Inlines.Count == 0)
                return false;
            if (Inlines.Count > 1)
                return false;
            var firstItem = Inlines[0];
            if (!(firstItem is string))
                return true;
            var text = firstItem as string;
            text = text.Trim();
            if (string.IsNullOrEmpty(text))
                return false;
            return true;
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            Lines.Arrange(new Rect(new Point(0, 0), finalSize));
            return finalSize;
        }

        protected override Size MeasureOverride(Size availableSize)
        {
            ClearLinks();
            Lines.Children.Clear();

            InitializeStack();

            double firstLineIndent = TextIndent >= 0d ? TextIndent : 0d;
            double hangingIndent = TextIndent < 0d ? -TextIndent : 0d;

            var line = new TextBlockPlusLine(this, firstLineIndent, availableSize.Width);
            Lines.Children.Add(line);
            while (_stack.Count != 0)
            {
                object item = _stack.Pop();

                if (item is LineBreak)
                {
                    line = new TextBlockPlusLine(this, hangingIndent, availableSize.Width);
                    Lines.Children.Add(line);
                }
                else if (item is Run || item is Link)
                {
                    var inline = item as Inline;
                    AddItem(_stack, line, inline.Clone(this));
                }
            }

            Lines.Measure(availableSize);
            return Lines.DesiredSize;
        }

        private Stack<object> _stack;

        private void InitializeStack()
        {
            _stack = new Stack<object>();
            if (!string.IsNullOrEmpty(Text))
            {
                var text = Text;
                if (AutoHyperlinkingEnabled)
                {
                    var autoLinkedInlines = GetAutoLinkedInlines(text);
                    for (var i = autoLinkedInlines.Count - 1; i >= 0; i--)
                        _stack.Push(autoLinkedInlines[i]);
                }
                else
                {
                    _stack.Push(new Run() { Text = text });
                }
            }
            for (var i = Inlines.Count - 1; i >= 0; i--)
            {
                var inline = Inlines[i];
                if (AutoHyperlinkingEnabled)
                {
                    if (inline is Run)
                    {
                        var text = ((Run) inline).Text;
                        var autoLinkedInlines = GetAutoLinkedInlines(text);
                        for (var j = autoLinkedInlines.Count - 1; j >= 0; j--)
                            _stack.Push(autoLinkedInlines[j]);
                    }
                    else
                    {
                        _stack.Push(inline);
                    }
                }
                else
                {
                    _stack.Push(inline);
                }
            }
        }

        private void AddItem<T>(Stack<object> stack, TextBlockPlusLine line, T inline) where T : Inline
        {
            Inline remainder;
            Inline clone = inline.Clone(this);
            if (line.Add(clone, out remainder))
            {
                if (clone is Link)
                {
                    _links.Add(clone as Link);
                    ListenToLink(clone as Link, true);
                }
            }
            else //current line is full?
            {
                if (line.Children.Count == 0)
                    throw new Exception("Couldn't add item");

                stack.Push(inline);
                stack.Push(new LineBreak());
                return;
            }
            if (remainder != null) //wrap to next line
            {
                stack.Push(remainder);
                stack.Push(new LineBreak());
                return;
            }
        }

        private List<Inline> GetAutoLinkedInlines(string messageText)
        {
            var autoLinkedInlines = new List<Inline>();

            var inlineUris = InlineUris.Get(messageText);
            var charIndex = 0;
            foreach (var inlineUri in inlineUris)
            {
                if (inlineUri.StartIndex > charIndex)
                {
                    //add text before link
                    var run = new Run() { Text = messageText.Substring(charIndex, inlineUri.StartIndex - charIndex) };
                    autoLinkedInlines.Add(run);
                    charIndex += run.Text.Length;
                }

                //add Hyperlink
                var link = new Link();
                var linkText = messageText.Substring(inlineUri.StartIndex, inlineUri.Length);
                link.Text = linkText;
                link.NavigateUri = new Uri(linkText);
                autoLinkedInlines.Add(link);
                charIndex += inlineUri.Length;
            }

            if (charIndex < messageText.Length)
            {
                //add text at end
                var run = new Run() { Text = messageText.Substring(charIndex) };
                autoLinkedInlines.Add(run);
            }

            return autoLinkedInlines;
        }

        private readonly List<Link> _links = new List<Link>();

        private void ClearLinks()
        {
            foreach (var hyperlink in _links)
            {
                ListenToLink(hyperlink, false);
            }
            _links.Clear();
        }

        private void ListenToLink(Link link, bool listen)
        {
            if (link == null)
                throw new ArgumentNullException("link");

            if (listen)
            {
                link.Click += link_Click;
            }
            else
            {
                link.Click -= link_Click;
            }
        }

        private void link_Click(object sender, RoutedEventArgs e)
        {
            var link = (Link)sender;
            OnLinkClicked(link);
        }

        private void OnLinkClicked(Link link)
        {
            if (LinkClicked != null)
                LinkClicked(link, new RoutedEventArgs());
        }
    }
}